RoR_chapter4 現実の複雑さに対応する
RoR_chapter4 現実の複雑さに対応する
マイグレーション
DBにインデックスを貼ったり修正したりする操作をもう少し深掘りする
以下の2ステップが基本。Laravelと同じ
DBスキーマを変更するmigrationファイルを作成
rails db:migrateで DBに適用する
Railsは基本的に3つの環境があるので、開発用、テスト用、本番用のものを使い分ける。
コマンドでマイグレーションするときもRAILS_ENV=productionなどで指定する方がよい
マイグレーションの履歴は、schema_migrationsテーブルに保管されている
またロールバックも同様に可能
railsがテーブルを作る処理のものを逆側に汲み取って、削除するような処理と判断してくれる
laravelのようにプログラマが削除する処理を書くこともできる
schema.rb
DBの構造が保管されているテーブルで、マイグレーションを適用すると自動で出力される
db:schema:dumpというコマンドで出力することもできる
マイグレーションでエラー発生時はlaravelのように途中で止まるのではなく、ロールバックしてくれる
データ内容の制限
taskleafのnameカラムに not null制約をつけよう
code:txt
bin/rails g migration ChangeTasksNameNotNull
作った後は、bin/rails db:migrateで反映させる。
カラムの詳細を変えるタイミング
今からtasksテーブルを作る段階ならば、テーブル作成時のmigrationを修正する
もう既に運用済みの場合ならば、変更専用のmigrationを作って対応するのが良い。
コマンドが動かない時
spring stopをやってみる。
マイグレーション適用テスト
code:txt
bin/rails s
irb(main):002:0> Task.new(name: nil).save
# 例外発生(not null制約)。
ActiveRecord::NotNullViolation (PG::NotNullViolation: ERROR: null value in column "name" of relation "tasks" violates not-null constraint)
# コンソールでこんな感じで作れる
Task.new(name: "consoleで。").save
文字列カラムの長さ指定などもできるので、必要になったら調べる。
ユニークインデックス
あるテーブルのカラムのデータが全レコードで一意だった時に使う
ユニークインデックスを作成することで一意性が壊されるのを防ぐことができる
重複した値がデータベースに存在することがなくなるため、便利
なおnull判定のものは全て異なる値と認識される
モデルの検証機能について
検証とは、いわゆるバリデーションのこと。
マイグレーションでのデータ値の規制だけでは、webで入力する値のエラー全てをカバーしきれない
not null制約や文字列が長すぎる場合の指定など、ユーザーにとっては不具合で止まったとしかわからない。
バリデーションとDBの制限を行なっておくことがかなり確実
検証の流れ
モデルをDB登録する前に検証を行い、エラーが起こった場合差し戻すという流れ
saveメソッドは自動で検証を行い、エラーならfalseを返す。
またこの時、errorsを通じてエラー詳細にアクセスすることができる
検証は、検証処理単独でも呼ぶことができる。
valid?メソッド(妥当であるか?というメソッド)
save!メソッドの!
検証エラー時に、falseを返すのではなく例外を発生させるという仕組み
例外が出なければDBへの登録が行われる流れ。予期しない失敗を防ぐことができる
検証をスルーする
task.save(validate: false)と書くことで検証をスルーできる
検証を書く
1. Railsが用意している検証用のヘルパを使う
2. 自分で任意の検証コードを書く
1. Railsが用意している検証用のヘルパを使う
必須ならpresence、文字列の指定ならlength: { maximum: 30 }というようにヘルパを用意してくれている
code:task.rb
class Task < ApplicationRecord
validates :name, presence: true
end
code:rb
irb(main):003:0> task = Task.new
=> #<Task id: nil, name: nil, description: nil, created_at: nil, updated_at: nil> irb(main):004:0> task.save
(0.2ms) BEGIN
(0.2ms) ROLLBACK
=> false
# errorsでエラー確認
irb(main):008:0> task.errors
## 文字だけ
irb(main):010:0> task.errors.full_messages
オブジェクトがDBに登録済みかどうかチェックする関数もある。
task.persisted?
入れた場合は成立するかもチェック。
code:rb
irb(main):014:0> task.name = "テストタスク"
irb(main):016:0> task.save
=> true
irb(main):018:0> task.persisted?
=> true
コントローラとビューで検証エラーへの対応をする
ユーザーにエラーの詳細を伝えるということ
上のバリデーションを貼って画面で更新すると、今こんな感じ
https://scrapbox.io/files/63ea30bd09eb47001b720f05.png
createメソッドを変更しよう
code:rb
def create
@task = Task.new(task_params)
if @task.save
redirect_to @rask, notice: "タスク「#{@task.name}」を登録しました。"
else
render :new
end
end
変えたところ
task.save!をtask.saveとした
成功したか失敗したかの戻り値によって処理を変更するように変更
!だと失敗した時falseが返るのではなく、例外処理が働くため
render :new
falseだった時、登録用のビューを呼んで再入力を促す。
@task = Task.newと、インスタンス変数に一旦代入させている
エラーで新規登録画面に戻す時、Taskオブジェクトを渡すため。
フォームへ送信したデータを引き継がせて入れたり、エラーの情報をビューに渡せる
フォームにエラーのメッセージを表示する項目を作る
code:slim
# errors.present? で、エラーの有無を調べて、あった場合は指定のhtmlタグを作る
- if task.errors.present?
ul#error_explanation
- task.errors.full_messages.each do |message|
li= message
2. 自分で任意の検証コードを書く
検証を行うメソッドを追加し、そのメソッドを検証用メソッドとして指定する方法
特定のモデル専用の処理を作りたいならこっち
自前のvalidatorを作って利用する方法
汎用的なものを書くときはこちら
今回は、「nameに,が入ってはいけない」というバリデーションを作る。
検証用メソッドをモデルクラスに登録し、実装する
code:task.rb
class Task < ApplicationRecord
validates :name, presence: true
validates :name, length: { maximum: 30 }
validate :validate_name_not_including_comma # methodを記述する
private
def validate_name_not_including_comma
errors.add(:name, 'にカンマを含めることはできません') if name&.include?(',')
end
end
検証メソッドの基本は、エラーに遭遇した場合、エラー内容を格納するという流れ
nameにカンマが入っているかどうかをぼっち演算子を使って行なっている
nameがnilの時は検証が通るようにしている(nameなんてものはありません、というエラーが出なくなる)
検証が行われない登録・更新の動作
saveメソッドを使えば自動で検証を行なってくれるが、行なってくれないDB保存メソッドもあるので注意。
コールバック
登録→更新→削除...というライフサイクル
このイベントの前後に任意の処理を挟むことができる。これをコールバックと呼ぶ
例えばnameが未記入の時、エラーで返すこともできるが、
「nameに"名前なし"というタイトルをつけておく」というコールバックを仕込むこともできる。
code:task.rb
class Task < ApplicationRecord
# 検証のサイクルの前に、set_nameless_nameメソッドを実行する
before_validation :set_nameless_name
...
private
def set_nameless_name
self.name = '名前なし' if name.blank?
end
ログイン機能の実装
セッションとcookie
違いについて
cookieはブラウザにだけ保存され、セッションはサーバ側にも情報が保存される
cookieはサイトを閉じても保存されるが、セッションはユーザーがサイトから離脱すると消える。
セッション
session[:user_id] = @user.idという感じで格納できる
cookie
railsではセッションの仕組みがcookieによって実現されている
cookieによってやりとりされるセッションをキーにして保管される
Userモデルの作成
code:rb
# bin/rails 作成(generate) 作成するもの 名前(単数) カラム1:タイプ カラム2:タイプ...
bin/rails g model user name:string email:string password_digest:string
Running via Spring preloader in process 85867
invoke active_record
create db/migrate/20230802125802_create_users.rb
create app/models/user.rb
invoke test_unit
create test/models/user_test.rb
create test/fixtures/users.yml
migrationファイルが作られるので、少し調整して反映する
bin/rails db:migrate
パスワードを暗号化して保管する
gem 'bcrypt', '~> 3.1.7'を使う。
元々記述があり、コメントアウトされているので解除してbundleでgemを読み込む。
ロールを分ける
Userモデルにadminフラグを追加して管理する
bin/rails g migration add_admin_to_users
code:add_admin_to_users.rb
class AddAdminToUsers < ActiveRecord::Migration5.2 def change
add_column :users, :admin, :boolean: default: false, null:false
end
end
bin/rails db:migrate rails db:migrateでもいける
ユーザー管理のためのコントローラを実装する
bin/rails g controller Admin::Users new edit show index
Adminというモジュール名前空間の中にUsersControllerというクラスを定義するという意味合い
railsではモジュール空間と、コード保存のためのディレクトリ構造とが対応している
app/controllers/admin/users_controller.rbという作りになるだけ
今回は管理者が「ユーザー」を管理するコントローラということでこういう作りにした
将来管理者がページ管理するならpage_controllerを作れたりと、拡張性が良い
ページを作っていく
td=とかth=の書き方は、td = というようなスペースを空けた書き方と同意味になる。
human_attribute_xxx
ja.ymlで定義した名称をこれで呼ぶことができる
td= link_to user.name, [:admin, user]
user.nameの名前を画面に出して
admin/users/1へのリンクを作成する
namespace: :adminにuserの情報を渡す
userの情報が渡ってきたので、railsがshowメソッドで反映させるように判断してるという挙動
taskのページもshowメソッドに渡す手順はtd= link_to task.name, taskなので多分そう
詳細画面(show)
td= @user.admin? ? 'あり': 'なし'
条件式 ? 真の時の値 : 偽の時の値
td= @user.admin ? 'あり': 'なし'でも動作はする
ガード節というらしい
例外的な条件で例外的な値を返すのであれば、処理の冒頭部分で条件判断とreturnを行って例外処理を完結させる
adminに0 or 1以外の値が場合はエラーにするというような感じか?
4-5-5 ログイン機能の実装
ログインするためのフォーム画面
画面から送られてきた情報を元にユーザーを認証する
ログアウト機能
table:ログイン
アクション内容 HTTPメソッド URL アクション名
ログインフォームを表示 GET /login new
フォーム情報を元に認証 POST /login create
ログアウト DELETE /logout destroy
フォーム画面を作る
Railsの場合、ログイン = セッションというリソースを作るという意味合いであることが多い
セッションを作るための処理を書くコントローラを作る
bin/rails g controller Sessions new
sessions_controller生成 newが書かれている
route.rbにget 'sessions/new'が記述
newのビューページが作成された
URLを変える
/sessions/ではなく/loginにしたい。
route.rbの記述を変える
get 'sessions/new' を
get '/login', to: 'sessions#new' にした。
ログインの実行
POSTメソッドのcreateアクションを作成する
code:route.rb
get '/login', to: 'sessions#new'
post '/login', to: 'sessions#create' # 追加
処理を書く
code:session_controller.rb
def create
# ユーザーをemailで取得
user = User.find_by(email: session_params:email) # パスワードの認証をauthenticateメソッドで行う
if user&.authenticate(session_params:password) redirect_to root_url, notice: 'ログインしました。'
else
render :new
end
end
private
# パラメータがsession{...}の形なのかをチェックし、その中から:email,:passwordのパラメータを抜き取る
# CSRF対策, 適当なパラメータが送られた場合、その情報で更新されてしまうのを防ぐ.
def session_params
params.require(:session).permit(:email, :password)
end
if user&.authenticate(session_params[:password])
authenticate
Userクラスにhas_secure_passwordと記述した際に自動で追加された認証メソッド
引数で受け取ったpasswordをハッシュ化する
その結果がUserオブジェクト内部に保存されているdigestと一致するかを調べる
暗号の分野では、暗号化(ダイジェスト)された文字列をメッセージダイジェストと呼びます。メッセージダイジェストは、元の文字列に戻すことができません。
User.password_digestカラムの値と比較して正しいかを検証してくれているだけ
一致していたらuserオブジェクト自身を返し、不一致であればfalseを返す
&.
ぼっち演算子
例えばuserが空の時
user.nameだとエラーになるが
user&.nameだとエラーにならない(nilを返す)
if後の処理について
session[:user_id]でセッションにuser_idを格納
つまりこの処理で、以下の形になった
誰もログインしていない場合は、session[:user_id]はnil
誰かがログインしている場合は、session[:user_id]にログイン中のユーザーのIDが入っている
ログイン情報の取得を簡単にする
ユーザーが既にログインしている場合、その情報を取得する
User.find_by(id: session[:user_id])
こういった処理はいろんな画面で使う(会員用ページなら全部書く必要があったりする)
そのため全てのコントローラから利用できるようにする
全てのコントローラはApplicationControllerから継承されているので、ここに書けば良い。
code:application_controller.rb
class ApplicationController < ActionController::Base
helper_method :current_user
private
def current_user
end
end
helper_method指定
全てのビューから呼ぶことができるようにする
current_user
||=
nilガード
もし@current_userに値が入っているならそれをそのまま使う
入っていないならばUser.find_by(id: ....の結果を入れる
ログアウトの実装
sessions[:user_id]にnilが入っていれば良い。
ルーティング記述
delete '/logout', to: 'sessions#destroy'
アクション記述
code:rb
def destroy
# session.delete(:user_id) でも良い
reset_session
redirect_to root_url, notice: 'ログアウトしました'
end
ビュー記述
code:rb
ul.navbar-nav.ml-auto
- if current_user
li.nav-item= link_to 'タスク一覧', tasks_path, class: 'nav-link'
li.nav-item= link_to 'ユーザー一覧', admin_users_path, class: 'nav-link'
li.nav-item= link_to 'ログアウト', logout_path, method: :delete, class: 'nav-link'
- else
li.nav-item= link_to 'ログイン', login_path, class: 'nav-link'
logout_pathとかについて
route.rbで/logoutと記載していたら、そこまでのパスはlogut_pathで定義できる
/logと記載していたら、log_pathで呼べる。railsがうまくやってくれている
? sessions:user_idは、複数入る前提なのか、それとも1つしか入らない前提なのか、ユーザーが個別に持つ情報なのか? セッションはサーバが持つ情報
ユーザーが離脱すると消える
明日もう一度見る
ログインした状態で時間を置くので消えているのかチェック
ログイン振り返り
code:html.slim
.container
こう書くとログインしているユーザーのIDを画面に出せる
ユーザーがログインしていればsession[user_id]に値があり、していなければnil
current_userの作り
ユーザーがログインしていればsessionに値があるので、その値でfindするというだけ
ログインしていなければ各ページへアクセスできないようにする
コントローラのフィルタ機能を使う
アクションを処理する前後に任意の処理を挟む事ができる。
middlewareと作りとしては同じ。
今回の仕様
ユーザーがログインしているかどうかを調べる
ログインしていれば遷移させ、していなければログイン画面にリダイレクトさせる。
タスクページだけに制限をかけたいのであればtasks_controller.rbに書けば良い
ただ今回はログイン判断という全ページで使う処理なので、application_controllerに記述する。
code:application_controller.rb
before_action :login_required
def login_required
redirect_to login_url unless current_user
end
全ての#actionが行われる前にこの処理が走る。
current_userヘルパを呼び出し、current_userに何も値が入っていないならログイン前と判断
ログイン画面の場合でもこの処理が行われてしまうので、ログイン関連の画面だけ許可する
code:rb
# スキップしたいコントローラに以下を記述する
skip_before_action :login_required
ログインユーザーのデータだけを扱えるようにする
taskleafにログインして見えるタスクは、そのユーザーが登録したタスクだけにしたい。そのために
UserとTaskを紐づける
tasks.user_idの追加
リレーションの定義
ログインしているユーザーに紐づくtaskデータを登録できるようにする
一覧・詳細・変更画面はログインユーザーに紐づくデータだけを扱う
UserとTaskの紐づけ
bin/rails g migration AddUserIdToTasks
code:migrate/20230907150009_add_user_id_to_tasks.rb
class AddUserIdToTasks < ActiveRecord::Migration5.2 def up
execute 'DELETE FROM tasks;'
add_reference :tasks, :user, null: false, index: true
end
def down
remove_reference :tasks, :user, index: true
end
end
既存のタスクを全部一旦消してから
add_referenceでtasksとuserを紐づける。
add_reference(テーブル名, リファレンス名, オプション)
tasksにuserをリファレンスさせ、nullを許可しないでインデックスを付与するという感じ
↑が複数形と単数系なのは、userに対してtasksは1対多だからっぽい
マイグレーションの実行
code:txt
rails db:migrate
関連(Assosication)について
DB上の紐付けを前提にして、Modelも紐付けの定義をする事ができる
今回はユーザーに対して、タスクが複数
code:rb
# task.rb
belongs_to :user
# user.rb
has_many :tasks
これで2通りのメソッドが使える
ユーザーが作ったタスク一覧参照したい場合はuser.tasksで取得ができる
タスクを作ったユーザーを確認したい場合はtask.userで取得ができる
タスク登録時の挙動を修正する
タスク登録時にuser_idを格納する
add_reference :tasks, :user, null: false, index: trueで作ったカラムに。
code:rb
# 今
@task = Task.new(task_params)
# 修正(2通り書ける)
@task = Task.new(task_params.merge(user_id: current_user.id)
@task = current_user.tasks.new(task_params) # 関連を宣言したので呼べる
mergeメソッド
param系統で使える
通常、railsはストロングパラメータという仕組みを採用している
Web上から受けつけたパラメータが、本当に安全なデータかどうかを検証した上で、取得するための仕組みです。Rails4から実装されています。
params.require(:task).permit(:name, :description)
基本上のように、フォームから渡された値をpermitで許可し、その値を通す
だがuser_idなどはフォームではなく、こちら側が勝手に決めた値なので通常通らない
mergeメソッドを使うことでパラメータで受け取った値ではないが追加したい値を格納することができる
タスクindex変更
@tasks = Task.all
これだとDBの全部のタスクを取得する動きになるので指定する
code:rb
@tasks = current_user.tasks # 関連で呼べるようになった
@tasks = Task.where(user_id: current_user.id)
どっちでも良い
カラムの真偽値を取得する
Userモデルの情報を引き出す
current_user.name?なら、名前が格納されているかどうかが真偽値で返ってくる
管理者用ページを作りたいなら、current_user.admin?でadminカラムのフラグを確認できる
ユーザー登録画面を管理者用の画面とする
users_controller.rbにrequire_adminというprivateメソッドを作成する
code:rb
class Admin::UsersController < ApplicationController
before_action :require_admin
...
private
...
def require_admin
redirect_to root_url unless current_user.admin?
end
end
これを書くとadmin/usersにリダイレクトする
ただしリダイレクトするということは存在を知られることでもあるので、ステータスコード404を返すようにしても良い。
初期データの考え方
ユーザー作成画面を管理者専用にした場合、最初の管理者はどうやって作るか
通常通り、コンソールでコードを実行して作れば良い。
もしくはseedを使う。
RailsのDB取得動作
起点 . 絞り込み条件 . 実行部分
User.where(admin:true).firstなど
起点
モデルを記述するのが基本。
assosicationを付けておけば、関連するモデルも起点として使用できる。
current_user.tasks
絞り込み条件
whereとかorderとか。特に直感的にわからないものだけ抜粋。
join
SQLのjoinを作成する。
none
何もヒットしない検索条件
DBに検索には行かないが、何もヒットしなかったということを明確にして処理を書く時に使う
実行部分
findとかfirstとか。
その他
to_sqlメソッド
データ取得処理に繋げることで生成予定のSQLを見ることができるので便利。
code:rb
logger.debug "【LOG】#{current_user.tasks.to_sql}}"
# 【LOG】SELECT "tasks".* FROM "tasks" WHERE "tasks"."user_id" = 4}
scopeの活用
絞り込みの条件を名づけてメソッドとして保管できる。
code:task.rb
class Task < ApplicationRecord
...
belongs_to :user
scope :recent, -> { order(created_at: :desc)}
private
...
end
code:rb
# @tasks = current_user.tasks.order(created_at: :desc) と同じ意味
@tasks = current_user.tasks.recent
taskコントローラにもフィルタを使う
task_controllerについて
show, edit, update, destroyのアクションで以下が書かれている
@task = current_user.tasks.find(params[:id])
変更するときに4箇所の修正が必要となり大変なので、フィルタ機能を使って処理をまとめる
フィルタについておさらい
before_actionのこと
上のリンクでは、アクションの処理を行う前にログインしているかどうかを挟んでいた
今回は show, edit, update, destroyのアクションの場合フィルタを挟むようにする
4-10 URLをリンクとして判断させる
gem 'rails_autolink'を使う
Gemfileに書いてbundle(もしくはbundle install)コマンドで導入する。
これでauto_link()めシッドが使えるようになった